home *** CD-ROM | disk | FTP | other *** search
/ Developer CD Series 2000 August: Tool Chest / Dev.CD Aug 00 TC Disk 2.toast / pc / sample code / overview / moreisbetter / mib-libraries / moremenus / moresystemmenus.c < prev    next >
Encoding:
Text File  |  2000-06-23  |  35.6 KB  |  1,197 lines

  1. /*
  2.     File:        MoreSystemMenus.c
  3.  
  4.     Contains:    Module for adding iconic menus to the right of the menu bar.
  5.  
  6.     Written by:    Quinn, Pete Gontier
  7.  
  8.     Copyright:    Copyright © 1999 by Apple Computer, Inc., All Rights Reserved.
  9.  
  10.                 You may incorporate this Apple sample source code into your program(s) without
  11.                 restriction. This Apple sample source code has been provided "AS IS" and the
  12.                 responsibility for its operation is yours. You are not permitted to redistribute
  13.                 this Apple sample source code as "Apple sample source code" after having made
  14.                 changes. If you're going to re-distribute the source, we require that you make
  15.                 it clear in the source that the code was descended from Apple sample source
  16.                 code, but that you've made changes.
  17.  
  18.     Change History (most recent first):
  19.  
  20.          <1>     25/8/99    Quinn   First checked in.
  21. */
  22.  
  23. // To Do Items
  24. //
  25. // √ Ask Eric about icon suite titles and menu bar offscreen always being 8 bits deep
  26. //   • Eric says it should be fixed in Sonata, but I can't test because I don't have
  27. //     a 1 bit capable machine that can run Sonata!
  28. // • get rid of NOPs in PPC callers
  29. //
  30. // Testing (perfunctorary, boot to Finder, test system menu, test that other system menus still work)
  31. //
  32. // - System 7.1 (fails with bus error at startup, GetMenu needs InitGraf)
  33. // √ System 7.5.5
  34. // • Mac OS 7.6.1
  35. // √ Mac OS 8.1
  36. // √ Mac OS 8.6
  37. // √ Mac OS 9.0
  38. // √ third party system menus
  39. //   √ Timbuktu
  40. //   √ MOM
  41. // √ 68K clients
  42. // √ PowerPC clients
  43. // √ multiple clients in different extensions
  44. // √ multiple clients in same extension
  45. // √ keyboard menu
  46. // √ JLK, especially Kotereri's menu
  47. // √ sub-menus
  48. // • MenuEvent
  49. // • modifying system menus while in the application context
  50.  
  51. /////////////////////////////////////////////////////////////////
  52.  
  53. // MIB Setup
  54.  
  55. #include "MoreSetup.h"
  56.  
  57. // Mac OS Interfaces
  58.  
  59. #include <MacTypes.h>
  60. #include <Menus.h>
  61. #include <LowMem.h>
  62. #include <MixedMode.h>
  63. #include <Traps.h>
  64.  
  65. // Wacky Metrowerks Interfaces
  66.  
  67. #include <A4Stuff.h>
  68. #include <SetUpA4.h>
  69.  
  70. // ANSI C Interfaces (just for "offsetof" macro)
  71.  
  72. #include <stddef.h>
  73.  
  74. // Our prototypes
  75.  
  76. #include "MoreSystemMenus.h"
  77.  
  78. /////////////////////////////////////////////////////////////////
  79. #pragma mark ----- Comments -----
  80.  
  81. /*    Some Overall Notes:
  82.  
  83.       o A4 Handling -- We remember A4 in our primary entry point
  84.           and reset it whenever any other entry point is called.
  85.           There are two other types of entry points, published routines
  86.           and trap patches.  You can spot the former by searching
  87.           for "extern pascal" and the latter by searching for "static pascal".
  88.           Note that we don't set up A4 on some of the trivial utility
  89.           routines because they access no globals.
  90.       
  91.       o Patching SystemMenu --
  92.       
  93.         Reviewer comment:
  94.         
  95.         One design tweak to consider: instead of using
  96.         MenuDisable to figure out which item was selected, you
  97.         could also patch SystemMenu, which is called to handle
  98.         selections from (you guessed it!) system menus as well
  99.         as DAs. This has a couple advantages:
  100.  
  101.         - you don't need your heuristic code for figuring out
  102.         when to ignore the contents of MenuDisable
  103.  
  104.         - MenuDisable is updated by the MDEF, not the Menu
  105.         Manager, and if someone wrote a custom MDEF for a system
  106.         menu, they might not know or bother to update
  107.         MenuDisable. But SystemMenu would still be called with
  108.         the proper info.
  109.  
  110.         Quinn's response:
  111.         
  112.         I thought about this for a while and decided against it.
  113.         The reasons:
  114.  
  115.         The more patches you have, the trickier the interaction
  116.         between them.  Specifically, the most obvious algorithm
  117.         for this would be...
  118.  
  119.               on menuSelect
  120.                 insert system menus
  121.                 call old menuSelect
  122.                 remove system menus
  123.               end menuSelect
  124.             
  125.               on systemMenu
  126.                 find and call client
  127.               end systemMenu
  128.  
  129.         but this is bad if the client callback recursively calls
  130.         MenuSelect.  I carefully avoid this in my current code
  131.         using...
  132.  
  133.               on menuSelect
  134.                 insert system menus
  135.                 call old menuSelect
  136.                 remove system menus
  137.                 find and call client
  138.               end menuSelect
  139.  
  140.         but it's hard to do this from two patches.
  141.  
  142.         There are two immediately obvious alternate designs,
  143.         shown below, but neither are particularly
  144.         satisfactorary.
  145.  
  146.         a)
  147.               on menuSelect
  148.                 insert system menus
  149.                 call old menuSelect
  150.                 if system menus still in menu bar
  151.                   remove system menus
  152.                 end-if
  153.               end menuSelect
  154.             
  155.               on systemMenu
  156.                 remove system menus
  157.                 find and call client
  158.               end systemMenu
  159.  
  160.         I didn't implement this because it involves removing
  161.         menus from the menu bar in the middle of MenuSelect's
  162.         call to SystemMenu, and I was worried about how Menu
  163.         Manager (past, present and future) might react to this.
  164.  
  165.         b)
  166.               on menuSelect
  167.                 insert system menus
  168.                 call old menuSelect
  169.                 remove system menus
  170.                 get global which holds menuResult from systemMenu
  171.                 find and call client
  172.               end menuSelect
  173.             
  174.               on systemMenu
  175.                 store menuResult in global
  176.               end systemMenu
  177.  
  178.         I didn't implement this because it involves more global
  179.         variables, which meant that I'd have to do yet another
  180.         recursion analysis.
  181.         
  182.         Besides, my current code works (-:
  183. */
  184.  
  185. /////////////////////////////////////////////////////////////////
  186. #pragma mark ----- Globals -----
  187.  
  188. // Simple static globals
  189.  
  190. static IconActionUPP     gDetachIconActionUPP;        // points to DetachIconAction
  191.  
  192. static UniversalProcPtr gMenuSelectOldUPP;
  193. static UniversalProcPtr gInsertMenuOldUPP;
  194.  
  195. // List of root menus
  196.  
  197. struct RootSysMenuEntry {
  198.     MenuRef                  theMenu;                // the menu to insert
  199.     SystemMenuCallbackProc      callback;                // the client callback
  200.     void                     *refcon;                // and its refcon
  201. };
  202. typedef struct RootSysMenuEntry RootSysMenuEntry;
  203. typedef RootSysMenuEntry *RootSysMenuPtr;
  204. typedef RootSysMenuPtr   *RootSysMenuHandle;
  205.  
  206. static RootSysMenuHandle gRootSysMenus = nil;        // an unbounded array of RootSysMenuEntry
  207.  
  208. static Boolean gHaveInsertedRootSysMenus = false;
  209.  
  210. static MCTableHandle gRootSysMenuCInfo = nil;
  211.  
  212. // List of sub-menus (built and torn down during the MenuSelect patch)
  213.  
  214. struct SubSysMenuEntry {
  215.     ItemCount    rootMenuIndex;                        // index into gRootSysMenus
  216.     MenuRef     parentMenu;                            // reference to parent menu
  217.     SInt16        itemInParent;                        // and item
  218.     MenuRef     childMenu;                            // reference to child menu
  219. };
  220. typedef struct SubSysMenuEntry SubSysMenuEntry;
  221. typedef SubSysMenuEntry *SubSysMenuPtr;
  222. typedef SubSysMenuPtr *SubSysMenuHandle;
  223.  
  224. static SubSysMenuHandle gSubSysMenus = nil;            // an unbounded array of SubSysMenuEntry
  225.  
  226. // State associated with the MenuSelect patch
  227.  
  228. typedef enum {
  229.     kMenuSelectStateNil  = 0,                        // outside of patch
  230.     kMenuSelectStatePre  = 1,                        // inside head part of patch
  231.     kMenuSelectStatePost = 2                        // inside of tail part of patch
  232. } MenuSelectState;
  233.  
  234. static MenuSelectState gMenuSelectState;
  235.  
  236. /////////////////////////////////////////////////////////////////
  237. #pragma mark ----- General Utility Routines -----
  238.  
  239. static Boolean ValidateSystemMenuRef(MenuRef theMenu)
  240.     // Returns true if theMenu is a valid system menu handle.
  241. {
  242.     Boolean result;
  243.     
  244.     // First check for nil and nil master pointer.
  245.     
  246.     result = (theMenu != nil) && (*theMenu != nil);
  247.     
  248.     // Then check that both handle and pointer are in the system heap.
  249.     // This will trigger falsely if you use the pageable system heap for menu
  250.     // handles, but third party developers shouldn't be doing that.
  251.     
  252.     if (result) {
  253.         THz sysZone;
  254.         char *lowerBound;
  255.         char *upperBound;
  256.         
  257.         sysZone = SystemZone();
  258.         lowerBound = (char *) sysZone;
  259.         upperBound = (char *) sysZone->bkLim;
  260.         
  261.         if (   ((char *)  theMenu) < lowerBound 
  262.             || ((char *)  theMenu) > upperBound
  263.             || ((char *) *theMenu) < lowerBound
  264.             || ((char *) *theMenu) > upperBound ) {
  265.             result = false;
  266.         }
  267.     }
  268.     
  269.     // Now check the handle size.
  270.     
  271.     if (result) {
  272.         Size size;
  273.  
  274.         size = GetHandleSize( (Handle) theMenu);
  275.         if (MemError() != noErr || size < offsetof(MenuInfo, menuData)) {
  276.             result = false;
  277.         }
  278.     }
  279.     
  280.     // Now check that neither resource nor purgeable bits are set.
  281.     
  282.     if (result) {
  283.         SInt8 s;
  284.         
  285.         s = HGetState( (Handle) theMenu );
  286.         if (MemError() != noErr || (s & 0x060) != 0) {
  287.             result = false;
  288.         }
  289.     }
  290.     
  291.     return result;
  292. }
  293.  
  294. static THz SetSystemZone(void)
  295.     // Sets the current zone to the system zone, returning
  296.     // the previous value.
  297. {
  298.     THz result;
  299.     
  300.     result = GetZone();
  301.     SetZone(SystemZone());
  302.     return result;
  303. }
  304.  
  305. static ItemCount CountRootSysMenus(void)
  306.     // Returns the number of root system menu entries in gRootSysMenus.
  307. {
  308.     ItemCount result;
  309.     MoreAssertQ(gRootSysMenus != nil);
  310.     
  311.     result = GetHandleSize( (Handle) gRootSysMenus ) / sizeof(RootSysMenuEntry);
  312.     MoreAssertQ(MemError() == noErr);
  313.     return result;
  314. }
  315.  
  316. static ItemCount CountSubSysMenus(SubSysMenuHandle subSysMenus)
  317.     // Returns the number of entries in the subSysMenus sub-menu list.
  318.     // Note that this handles the case where subSysMenus is nil and returns
  319.     // zero, which is unlike CountRootSysMenus.  I did this
  320.     // to fix the case where HandleMenuSelect is called with no
  321.     // sub-menus installed.
  322. {
  323.     ItemCount result;
  324.     
  325.     if (subSysMenus == nil) {
  326.         result = 0;
  327.     } else {
  328.         result = GetHandleSize( (Handle) subSysMenus ) / sizeof(SubSysMenuEntry);
  329.     }
  330.     return result;
  331. }
  332.  
  333. enum {
  334.     kHighestGlobalMenuID = -16385,
  335.     kLowestGlobalMenuID  = -20480
  336. };
  337.  
  338. static Boolean IsSystemMenuID(SInt16 menuID)
  339.     // Returns true if menuID is in the system menu range,
  340.     // as defined by the above constants.
  341.     //
  342.     // Don't blame me for the backwards logic in this routine.
  343.     // I just stole the code from PeteG (-:
  344. {
  345.     return !(menuID < kLowestGlobalMenuID || menuID > kHighestGlobalMenuID);
  346. }
  347.  
  348. static Handle LMGetSystemMenuList(void)
  349.     // Returns the low-memory global SystemMenuList,
  350.     // which is like MenuList but it holds the list
  351.     // of system menus.
  352. {
  353.     return *((Handle *) 0x0286);
  354. }
  355.  
  356. static MenuHandle GetSystemMenuHandle(SInt16 menuID)
  357.     // An analogue of GetMenuHandle, this routine
  358.     // returns a handle to an installed system menu
  359.     // in the system menu list.
  360. {
  361.     MenuHandle result;
  362.     Handle saveMenuList;
  363.     
  364.     // Switch MenuList to SystemMenuList, call
  365.     // GetMenuHandle and then switch it back.
  366.     
  367.     saveMenuList = LMGetMenuList();
  368.     LMSetMenuList(LMGetSystemMenuList());
  369.     
  370.     result = GetMenuHandle(menuID);
  371.  
  372.     LMSetMenuList(saveMenuList);
  373.  
  374.     return result;
  375. }
  376.  
  377. static SInt16 FindUniqueSystemMenuID(void)
  378.     // Returns the next available system menu ID.
  379.     // It works by starting at the top of the system
  380.     // menu ID range and calling GetSystemMenuHandle on
  381.     // each sequential ID to see whether that ID is already
  382.     // used.
  383.     //
  384.     // We call this routine when we insert our system
  385.     // menus into the system menu list at Process Manager
  386.     // launch time.
  387. {
  388.     SInt16 result;
  389.     
  390.     result = kHighestGlobalMenuID;
  391.     while ( GetSystemMenuHandle(result) != nil ) {
  392.         result -= 1;
  393.     }
  394.     
  395.     // The following assert fires if a) the system menu list
  396.     // is full, ie we have more than 4000 system menus,
  397.     // ie *very* unlikely, or b) something bad has happened.
  398.     // Either way, we want to know.  Because the only 
  399.     // valid failure case is so unlikely, the higher
  400.     // level ignore that possibility, so we don't have
  401.     // an error return from this routine.
  402.     
  403.     MoreAssertQ(result >= kLowestGlobalMenuID);
  404.  
  405.     return result;
  406. }
  407.  
  408. static SInt16 FindUniqueSubMenuID(void)
  409.     // Returns the next available menu ID for a hierarchical
  410.     // (or sub-) menu.  Prior to Mac OS 8.0, the sub-menu
  411.     // ID for a menu item was encoded in the item mark field
  412.     // of the menu, which meant that sub-menus were required
  413.     // to use an ID that fit into a byte.  This routine will
  414.     // search for such an ID and return it.
  415.     //
  416.     // We call this routine when we insert sub-menus into
  417.     // the menu bar, namely on our head patch to MenuSelect.
  418.     // Note that sub-menus go into the normal menu bar
  419.     // (because we remove them on the tail patch to MenuSelect)
  420.     // so this routine calls the standard GetMenuHandle to test
  421.     // for uniqueness.
  422. {
  423.     SInt16 result;
  424.     
  425.     result = 235;
  426.     while ( GetMenuHandle(result) != nil) {
  427.         result -= 1;
  428.     }
  429.     
  430.     // The following assert fires if a) the application's menu list
  431.     // is full, ie we have more than 250 application menus,
  432.     // ie *very* unlikely, or b) something bad has happened.
  433.     // Either way, we want to know.  Because the only 
  434.     // valid failure case is so unlikely, the higher
  435.     // level ignore that possibility, so we don't have
  436.     // an error return from this routine.
  437.  
  438.     MoreAssertQ(result > 0);
  439.  
  440.     return result;
  441. }
  442.  
  443. static Boolean IsSystemStartup(void)
  444.     // Returns true if we're still at system startup time.
  445.     // We do this in the traditional way, by testing the first
  446.     // byte of CurApName.  We only use this for debugging.
  447. {
  448.     return LMGetCurApName()[0] == 0xFF;
  449. }
  450.  
  451. extern pascal MenuRef GetNewSystemMenu(SInt16 menuResourceID)
  452.     // See comment in interface part.
  453. {
  454.     MenuRef result;
  455.     THz oldZone;
  456.     
  457.     oldZone = SetSystemZone();
  458.     result = GetMenu(menuResourceID);
  459.     if (result != nil) {
  460.         DetachResource( (Handle) result );
  461.         MoreAssertQ(ResError() == noErr);
  462.         
  463.         MoreAssertQ(ValidateSystemMenuRef(result));
  464.     }
  465.     SetZone(oldZone);
  466.     return result;
  467. }
  468.  
  469. extern pascal MenuRef NewSystemMenu(ConstStr255Param title)
  470.     // See comment in interface part.
  471. {
  472.     MenuRef result;
  473.     THz oldZone;
  474.     
  475.     MoreAssertQ(title != nil);
  476.     
  477.     oldZone = SetSystemZone();
  478.     result = NewMenu(0, title);
  479.     MoreAssertQ(result == nil || ValidateSystemMenuRef(result));
  480.     SetZone(oldZone);
  481.     return result;
  482. }
  483.  
  484. static pascal OSErr DetachIconAction(ResType theType, Handle *theIcon, void *yourDataPtr)
  485.     // GetDetachedIconSuite passes this routine to
  486.     // ForEachIconDo to detach each icon icon in the
  487.     // suite from the resource file from which it came.
  488. {
  489.     MoreAssertQ(theIcon != nil);
  490.     #pragma unused(theType)
  491.     #pragma unused(yourDataPtr)
  492.     
  493.     if (*theIcon != nil) {
  494.         DetachResource(*theIcon);
  495.  
  496.         // Errors from DetachResource probably aren't fatal
  497.         // (it probably means that the icon handle isn't a resource),
  498.         // but we certainly want to know about them.
  499.  
  500.         MoreAssertQ(ResError() == noErr);
  501.     }
  502.     
  503.     return noErr;
  504. }
  505.  
  506. extern pascal OSStatus GetDetachedIconSuite(IconSuiteRef *theIconSuite, SInt16 theResID, IconSelectorValue selector)
  507.     // See comment in interface part.
  508. {
  509.     long oldA4;
  510.     OSStatus err;
  511.     THz oldZone;
  512.     
  513.     oldA4 = SetUpA4();
  514.     
  515.     MoreAssertQ(theIconSuite != nil);
  516.     
  517.     oldZone = SetSystemZone();
  518.  
  519.     // The algorithm here is perfectly simple.  First get the icon suite,
  520.     // then iterate over its contents detaching each handle.  I looked
  521.     // at the Icon Services code and this seems like a perfectly reasonable
  522.     // strategy.  It also disassembled SBGetDetachIconSuite and it works
  523.     // in the same way.
  524.     
  525.     err = GetIconSuite(theIconSuite, theResID, selector);
  526.     if (err == noErr) {
  527.         (void) ForEachIconDo(*theIconSuite, kSelectorAllAvailableData, gDetachIconActionUPP, nil);
  528.     }
  529.     
  530.     SetZone(oldZone);
  531.     
  532.     (void) SetA4(oldA4);
  533.     
  534.     return err;
  535. }
  536.  
  537. extern pascal void IconSuiteToSystemMenuTitle(IconSuiteRef theIconSuite, StringPtr menuTitle)
  538.     // See comment in interface part.
  539.     //
  540.     // It's vital that this routine not more or purge memory unless you
  541.     // want to rewrite SetMenuTitleToIconSuite to lock the menu handle.
  542. {
  543.     MoreAssertQ(theIconSuite != nil);
  544.     MoreAssertQ(menuTitle != nil);
  545.  
  546.     // The following 2 are byte accesses, so they'll work
  547.     // on original 68000.
  548.     
  549.     menuTitle[0] = 5;
  550.     menuTitle[1] = 1;
  551.     
  552.     // The next is a 4-byte access, but it's 2-byte aligned
  553.     // so it will also work on the original 68000.
  554.     
  555.     *((IconSuiteRef *) &menuTitle[2]) = theIconSuite;
  556. }
  557.  
  558. extern pascal OSStatus SetMenuTitleToIconSuite(MenuRef theMenu, IconSuiteRef theIconSuite)
  559.     // See comment in interface part.
  560. {
  561.     OSStatus err;
  562.     
  563.     MoreAssertQ(ValidateSystemMenuRef(theMenu));
  564.     MoreAssertQ(theIconSuite != nil);
  565.     
  566.     if ( (**theMenu).menuData[0] == 5 ) {
  567.         IconSuiteToSystemMenuTitle(theIconSuite, (**theMenu).menuData);
  568.         err = noErr;
  569.     } else {
  570.         err = paramErr;
  571.     }
  572.     
  573.     return err;
  574. }
  575.  
  576. /////////////////////////////////////////////////////////////////
  577. #pragma mark ----- Core System Menu -----
  578.  
  579. extern pascal OSStatus InsertSystemMenu(MenuRef theMenu,
  580.                                         SystemMenuCallbackProc callback,
  581.                                         void *refcon)
  582.     // See comment in interface part.
  583. {
  584.     long oldA4;
  585.     OSStatus err;
  586.     RootSysMenuEntry newEntry;
  587.     
  588.     oldA4 = SetUpA4();
  589.  
  590.     MoreAssertQ(ValidateSystemMenuRef(theMenu));
  591.     MoreAssertQ(callback != nil);
  592.  
  593.     // If either of these fire, you're too late to call this routine,
  594.     // either because system startup is complete or because someone
  595.     // has already inserted a system menu.
  596.  
  597.     MoreAssertQ( IsSystemStartup() );
  598.     MoreAssertQ( ! gHaveInsertedRootSysMenus );
  599.  
  600.     // Fill out the new entry.
  601.     
  602.     newEntry.theMenu  = theMenu;
  603.     newEntry.callback = callback;
  604.     newEntry.refcon   = refcon;
  605.     
  606.     // Add the entry on to the system menu list.  Later on, in the
  607.     // InsertMenuPatch, we'll add it to the menu bar.
  608.     
  609.     if (theMenu == nil || callback == nil) {
  610.         err = paramErr;
  611.     } else {
  612.         err = PtrAndHand(&newEntry, (Handle) gRootSysMenus, sizeof(newEntry));
  613.     }
  614.  
  615.     (void) SetA4(oldA4);
  616.     
  617.     return err;
  618. }
  619.  
  620. // kRootMenuNotFound is used by FindRootMenuByID and FindRootMenuByRef
  621. // to indicate that no menu was found.  Note that those routines return
  622. // an ItemCount, which is unsigned, to returning -1 gets very confusing
  623. // very quickly (-:
  624.  
  625. enum {
  626.     kRootMenuNotFound = 0xFFFFFFFF
  627. };
  628.  
  629. static ItemCount FindRootMenuByID(SInt16 menuID)
  630.     // Search the list of root system menus (gRootSysMenus)
  631.     // looking for one with the given menu ID.  Returns
  632.     // kRootMenuNotFound if there is none.
  633. {
  634.     ItemCount result;
  635.     ItemCount systemMenuCount;
  636.     ItemCount systemMenuIndex;
  637.  
  638.     result = kRootMenuNotFound;
  639.     systemMenuCount = CountRootSysMenus();
  640.     for (systemMenuIndex = 0; systemMenuIndex < systemMenuCount; systemMenuIndex++) {
  641.         if (menuID == (**(*gRootSysMenus)[systemMenuIndex].theMenu).menuID) {
  642.             result = systemMenuIndex;
  643.             break;
  644.         }
  645.     }
  646.     return result;
  647. }
  648.  
  649. static ItemCount FindRootMenuByRef(MenuRef menuRef)
  650.     // Search the list of root system menus (gRootSysMenus)
  651.     // looking for one with the given menu ID.  Returns
  652.     // kRootMenuNotFound if there is none.
  653. {
  654.     ItemCount result;
  655.     ItemCount systemMenuCount;
  656.     ItemCount systemMenuIndex;
  657.  
  658.     MoreAssertQ(ValidateSystemMenuRef(menuRef));
  659.     
  660.     result = kRootMenuNotFound;
  661.     systemMenuCount = CountRootSysMenus();
  662.     for (systemMenuIndex = 0; systemMenuIndex < systemMenuCount; systemMenuIndex++) {
  663.         if (menuRef == (*gRootSysMenus)[systemMenuIndex].theMenu) {
  664.             result = systemMenuIndex;
  665.             break;
  666.         }
  667.     }
  668.     return result;
  669. }
  670.  
  671. extern pascal OSStatus InsertSystemSubMenu(MenuRef rootMenu,
  672.                                         MenuRef parentMenu, UInt16 itemInParent, MenuRef childMenu)
  673.     // See comment in interface part.
  674. {
  675.     long oldA4;
  676.     OSStatus err;
  677.     SubSysMenuEntry newEntry;
  678.     SInt16 newIDForChildMenu;
  679.     
  680.     oldA4 = SetUpA4();
  681.  
  682.     MoreAssertQ(ValidateSystemMenuRef(rootMenu  ));
  683.     MoreAssertQ(ValidateSystemMenuRef(parentMenu));
  684.     MoreAssertQ(ValidateSystemMenuRef(childMenu ));
  685.     
  686.     MoreAssertQ(gMenuSelectState = kMenuSelectStatePre);
  687.     
  688.     // Validate parameters
  689.     
  690.     err = noErr;
  691.     if ((rootMenu == nil) || (parentMenu == nil) || (childMenu == nil)) {
  692.         err = paramErr;
  693.     }
  694.     if (err == noErr && ((itemInParent == 0) || (itemInParent > CountMenuItems(parentMenu)))) {
  695.         err = paramErr;
  696.     }
  697.  
  698.     // Make sure rootMenu references a current system menu.
  699.     // If it does, record the index of the root menu for
  700.     // later use by HandleMenuSelect.  Also fill out the rest
  701.     // of the fields of the sub-menu entry.
  702.  
  703.     if (err == noErr) {
  704.         newEntry.rootMenuIndex = FindRootMenuByRef(rootMenu);
  705.         newEntry.parentMenu    = parentMenu;
  706.         newEntry.itemInParent  = itemInParent;
  707.         newEntry.childMenu     = childMenu;
  708.         
  709.         if (newEntry.rootMenuIndex == kRootMenuNotFound) {
  710.             err = paramErr;
  711.         }
  712.     }
  713.     
  714.     // Add newEntry to the sub-menu list, creating the sub-menu list if necessary.
  715.  
  716.     if (err == noErr) {
  717.         if (gSubSysMenus == nil) {
  718.             gSubSysMenus = (SubSysMenuHandle) NewHandleSys(0);
  719.             err = MemError();
  720.         }
  721.     }
  722.     if (err == noErr) {
  723.         err = PtrAndHand(&newEntry, (Handle) gSubSysMenus, sizeof(newEntry));
  724.     }
  725.     
  726.     // Finally, insert the menu with a unique ID into the menu bar,
  727.     // and set the parent item to reference its ID.
  728.     
  729.     if (err == noErr) {
  730.         newIDForChildMenu = FindUniqueSubMenuID();
  731.         (**childMenu).menuID = newIDForChildMenu;
  732.         InsertMenu(childMenu, hierMenu);
  733.  
  734.         SetItemCmd(parentMenu, itemInParent, hMenuCmd);
  735.         SetItemMark(parentMenu, itemInParent, newIDForChildMenu);
  736.         
  737.         // I added this after testing revealed that adding a sub-menu didn't 
  738.         // force the menu size to be recalculated, so the item text of the
  739.         // hierarchical item in the sub-menu was being truncated.  This
  740.         // only appears to happen on Mac OS 8.5, but the fix is sufficiently
  741.         // benign to be employed on all systems.
  742.         
  743.         CalcMenuSize(parentMenu);
  744.     }
  745.     
  746.     (void) SetA4(oldA4);
  747.     
  748.     return err;
  749. }
  750.  
  751. static void DeleteSubMenus(SubSysMenuHandle subSysMenus)
  752.     // Remove any sub-menus that we added to the menu bar.
  753.     // Basically this just walks subSysMenus (backwards, which
  754.     // isn't strictly necessary, but reassures me that the menu
  755.     // bar is somewhat consistent at each step), deleting each
  756.     // menu from the menu bar and reseting its parent to reference
  757.     // menu ID 0.
  758. {
  759.     ItemCount entryCount;
  760.     ItemCount entryIndex;
  761.     SubSysMenuEntry thisEntry;
  762.     
  763.     // Have to handle both nil and non-nil case.
  764.     // This expression always evaluates to true,
  765.     // but it captures the semantics of what this
  766.     // routine must do.
  767.     
  768.     MoreAssertQ(subSysMenus != nil || subSysMenus == nil);
  769.     
  770.     entryCount = CountSubSysMenus(subSysMenus);
  771.     for (entryIndex = 0; entryIndex < entryCount; entryIndex++) {
  772.         thisEntry = (*subSysMenus)[entryCount - entryIndex - 1];
  773.         DeleteMenu( (**(thisEntry.childMenu)).menuID );
  774.         SetItemMark( thisEntry.parentMenu, thisEntry.itemInParent, 0);
  775.  
  776.         // Recalculate the parent menu size, for consistency with
  777.         // the similar code in InsertSystemSubMenu.
  778.         
  779.         CalcMenuSize(thisEntry.parentMenu);
  780.     }
  781. }
  782.     
  783. static void CallSingleClient(ItemCount systemMenuIndex, OSType selector, MenuRef theMenu, void *param)
  784.     // Call the client's callback associated with the root menu
  785.     // whose index is systemMenuIndex with the supplied parameters.
  786.     // Note that the MenuRef is a parameter to this routine because,
  787.     // in the case of a sub-menu, it's the MenuRef of the sub-menu,
  788.     // not the MenuRef of the root menu.
  789. {
  790.     OSStatus junk;
  791.     RootSysMenuEntry *entryToCall;
  792.     
  793.     MoreAssertQ(ValidateSystemMenuRef(theMenu));
  794.     
  795.     entryToCall = &(*gRootSysMenus)[systemMenuIndex];
  796.     
  797.     junk = (entryToCall->callback)(selector, theMenu, param, entryToCall->refcon);
  798.     
  799.     // If the client returns an error, that's a problem because there
  800.     // are currently no selectors where the return result is used.
  801.     
  802.     MoreAssertQ(junk == noErr);
  803. }
  804.  
  805. static void CallAllClients(OSType selector, void *param)
  806.     // Call the callbacks associated with all root menus
  807.     // with the supplied parameters.  This routine is used
  808.     // for broadcast-type messages, for example kSystemMenuPreSelectAdjust.
  809.     // There's no MenuRef passed to this routine because the client
  810.     // is expected to adjust their sub-menus when they get called
  811.     // for their root menu.
  812. {
  813.     ItemCount systemMenuCount;
  814.     ItemCount systemMenuIndex;
  815.  
  816.     systemMenuCount = CountRootSysMenus();
  817.     for (systemMenuIndex = 0; systemMenuIndex < systemMenuCount; systemMenuIndex++) {
  818.         CallSingleClient(systemMenuIndex, selector, (*gRootSysMenus)[systemMenuIndex].theMenu, param);
  819.     }
  820. }
  821.  
  822. static SInt32 HandleMenuSelect(SInt32 menuAndItem, SubSysMenuHandle subSysMenus)
  823.     // This routine is called from the tail patch on MenuSelect
  824.     // to handle an actual menu command being returned.
  825.     // menuAndItem was the result from the original MenuSelect, and
  826.     // the result of this routine will be returned from our MenuSelect
  827.     // patch.
  828.     //
  829.     // The basic idea is that, if the menu command was on
  830.     // a system menu belong to us, we call the appropriate client
  831.     // callback routine and return 0 so that the client sees
  832.     // a null selection.  If the menu command doesn't belong to us,
  833.     // we just return the value passed in.
  834. {
  835.     SInt32 result;
  836.     SInt16 hitMenuID;
  837.     UInt16 hitItem;
  838.     ItemCount foundIndex;
  839.     MenuRef foundMenuRef;
  840.     
  841.     // The system implementation of MenuSelect will return 0 if the hit item
  842.     // is in a system menu.  Fortunately, the real hit item is still around
  843.     // in the low memory global MenuDisable.  So we use if menuAndItem is zero.
  844.     // Also note that we assign result before doing this, so we only use
  845.     // MenuDisable if it relates to a system menu and never return it from
  846.     // this routine, and hence from our patch on MenuSelect.
  847.     
  848.     result = menuAndItem;    
  849.     if (menuAndItem == 0) {
  850.         menuAndItem = LMGetMenuDisable();
  851.     }
  852.     
  853.     hitMenuID = menuAndItem >> 16;
  854.     hitItem   = menuAndItem & 0x0FFFF;
  855.     
  856.     // Search our list of system menus looking for a matching menu ID.
  857.     // Start by looking for a root menu and, if that fails, look for
  858.     // one of the sub-menus we attached to the root menus.
  859.     //
  860.     // If we find a matching menu, call the corresponding client with a
  861.     // kSystemMenuActOnChosen message, and also set menuAndItem to 0 so
  862.     // that the application never sees the menu choice.
  863.     
  864.     foundMenuRef = nil;
  865.     
  866.     foundIndex = FindRootMenuByID(hitMenuID);
  867.     if (foundIndex != kRootMenuNotFound) {
  868.         foundMenuRef = (*gRootSysMenus)[foundIndex].theMenu;
  869.     } else {
  870.         ItemCount entryCount;
  871.         ItemCount entryIndex;
  872.         
  873.         entryCount = CountSubSysMenus(subSysMenus);
  874.         for (entryIndex = 0; entryIndex < entryCount; entryIndex++) {
  875.             if (hitMenuID == (**(*subSysMenus)[entryIndex].childMenu).menuID) {
  876.                 foundIndex = (*subSysMenus)[entryIndex].rootMenuIndex;
  877.                 foundMenuRef = (*subSysMenus)[entryIndex].childMenu;
  878.                 MoreAssertQ(foundIndex < CountRootSysMenus());
  879.                 break;
  880.             }
  881.         }
  882.     }
  883.     if (foundIndex != kRootMenuNotFound) {
  884.         Boolean enabled;
  885.         CharParameter cmdOfHitItem;
  886.  
  887.         MoreAssertQ(ValidateSystemMenuRef(foundMenuRef));
  888.         
  889.         // Because we're using MenuDisable to get the actual item hit,
  890.         // we'll often get false positives, hits on the menu title or
  891.         // when the menu item is disabled.  Make sure we don't call the
  892.         // client in these cases.
  893.  
  894.         // Item 0 is the title and is never enabled.  Items from 1 to 31
  895.         // are controlled by the enableFlags field of the menu.  Items
  896.         // above 31 are always assumed to be enabled.
  897.         //
  898.         // Note that I don't use IsMenuItemEnabled (introduced in Mac OS 8.5)
  899.         // to access this information because it's a CFM-only routine.  It's
  900.         // only relevant for items beyond 31, which is a corner case IMHO.
  901.  
  902.         // Also, items that are the root of sub-menus (ie their cmd is
  903.         // hMenuCmd) can not be chosen.
  904.         
  905.         if (hitItem == 0) {
  906.             enabled = false;
  907.         } else if (hitItem <= 31) {
  908.             enabled = (((**foundMenuRef).enableFlags & (1L << hitItem)) != 0);
  909.         } else {
  910.             enabled = true;
  911.         }
  912.         if (enabled) {
  913.             GetItemCmd(foundMenuRef, hitItem, &cmdOfHitItem);
  914.             enabled = (cmdOfHitItem != hMenuCmd);
  915.         }
  916.         if (enabled) {
  917.             CallSingleClient(foundIndex, kSystemMenuActOnChosen, foundMenuRef, (void *) hitItem);
  918.         }
  919.         result = 0;
  920.     }
  921.     
  922.     return result;
  923. }
  924.  
  925. /////////////////////////////////////////////////////////////////
  926. #pragma mark ----- Trap Patches -----
  927.  
  928. enum {
  929.     uppMenuSelectProcInfo = 0x00F0,            // Extracted from InterfaceLib
  930.     uppInsertMenuProcInfo = 0x02C0            // Extracted from InterfaceLib
  931. };
  932.  
  933. typedef pascal SInt32 (*MenuSelectProcPtr)(Point startPt);
  934. typedef pascal void   (*InsertMenuProcPtr)(MenuHandle theMenu, SInt16 beforeID);
  935.  
  936. #if TARGET_RT_MAC_CFM
  937.     #define CallMenuSelectProc(userRoutine, startPt)  \
  938.             CALL_ONE_PARAMETER_UPP((userRoutine), uppMenuSelectProcInfo, (startPt))
  939.     #define CallInsertMenuProc(userRoutine, theMenu, beforeID)  \
  940.             CALL_TWO_PARAMETER_UPP((userRoutine), uppInsertMenuProcInfo, (theMenu), (beforeID))
  941. #else
  942.     #define CallMenuSelectProc(userRoutine, startPt)  \
  943.             ((MenuSelectProcPtr)(userRoutine))((startPt))
  944.     #define CallInsertMenuProc(userRoutine, theMenu, beforeID)  \
  945.             ((InsertMenuProcPtr)(userRoutine))((theMenu), (beforeID))
  946. #endif
  947.  
  948. static pascal SInt32 MenuSelectPatch(Point startPt)
  949.     // This is our patch on MenuSelect.  The basic idea is to
  950.     // call our clients to let them adjust their menus (and install
  951.     // sub-menus by calling InsertSystemMenu), call through to
  952.     // the old trap, call our clients to un-adjust their menus,
  953.     // and then call HandleMenuSelect to see whether the chosen
  954.     // menu was one of our system menus.  If it was, HandleMenuSelect
  955.     // calls the client to tell them that their menu was chosen and
  956.     // returns 0 so that we tell the host application that nothing
  957.     // happened.
  958.     //
  959.     // Note that MenuSelect is called by MenuEvent, so this patch
  960.     // covers that case as well.
  961. {
  962.     long oldA4;
  963.     SInt32 result;
  964.     SubSysMenuHandle tempSubSysMenus;
  965.     
  966.     oldA4 = SetUpA4();
  967.     
  968.     // Our patch should only be installed if InitMoreSystemMenus was successful.
  969.     MoreAssertQ(gRootSysMenus != nil);
  970.  
  971.     // Only do stuff if we're actually up and running.
  972.     // If gHaveInsertedRootSysMenus is false, it means that
  973.     // someone is calling MenuSelect before our InsertMenu patch
  974.     // has run, ie before anyone has inserted any system menus,
  975.     // ie before the Process Manager has launched.  I don't know
  976.     // whether this ever happens, I'm use being cautious.
  977.     
  978.     // Also, if gMenuSelectState is not nil then we're getting recursion
  979.     // where we're not expecting it (probably from the kSystemMenuPreSelectAdjust
  980.     // or kSystemMenuPostSelectAdjust callbacks, but possibly from another
  981.     // trap patch), so we just call through to the system in that case.
  982.     
  983.     // We want debug versions to tell the client that they're recursing
  984.     // in an unsupport fashion.
  985.     
  986.     MoreAssertQ(gMenuSelectState == kMenuSelectStateNil);
  987.     
  988.     if ( gHaveInsertedRootSysMenus && gMenuSelectState == kMenuSelectStateNil) {
  989.     
  990.         // OK, so we're up and running, so let's do our stuff.
  991.         
  992.         // First tell all clients that we're about to do a MenuSelect
  993.         // (they can adjust their menus and insert sub-menus)...
  994.         
  995.         MoreAssertQ(gSubSysMenus == nil);
  996.         gMenuSelectState = kMenuSelectStatePre;
  997.         CallAllClients(kSystemMenuPreSelectAdjust, nil);
  998.         
  999.         // ... then call through to the old trap address...
  1000.         
  1001.         result = CallMenuSelectProc(gMenuSelectOldUPP, startPt);
  1002.         
  1003.         // ... and then tell all clients that we're done...
  1004.         
  1005.         gMenuSelectState = kMenuSelectStatePost;
  1006.         CallAllClients(kSystemMenuPostSelectAdjust, nil);
  1007.         
  1008.         // Things get tricky here.  In order to allow the client
  1009.         // to post modal dialogs from its kSystemMenuActOnChosen callback,
  1010.         // we must be re-entrant at the time we call HandleMenuSelect
  1011.         // (because the ModalDialog routine will call MenuSelect).
  1012.         // But we want to avoid having sub-menus in the menu bar when
  1013.         // we're recursively called because we're going to call the
  1014.         // client's kSystemMenuPreSelectAdjust callback, which will
  1015.         // attempt to re-insert those menus.
  1016.         //
  1017.         // So, we remove our sub-menus now, before calling the
  1018.         // kSystemMenuActOnChosen callback, then we make the callback,
  1019.         // and then we dispose of the memory we used to track the
  1020.         // sub-menus.
  1021.         
  1022.         tempSubSysMenus = gSubSysMenus;
  1023.         gSubSysMenus = nil;
  1024.         
  1025.         DeleteSubMenus(tempSubSysMenus);
  1026.  
  1027.         gMenuSelectState = kMenuSelectStateNil;
  1028.  
  1029.         // At this point we're prepared to accept recursion.
  1030.         // Call HandleMenuSelect to process the results
  1031.         // from the menu operation and call the appropriate
  1032.         // client if it was one of our menus.
  1033.         
  1034.         result = HandleMenuSelect(result, tempSubSysMenus);
  1035.  
  1036.         // Clean up.  If we created any sub-menu info,
  1037.         // dispose of it.
  1038.         
  1039.         if (tempSubSysMenus != nil) {
  1040.             DisposeHandle( (Handle) tempSubSysMenus);
  1041.             MoreAssertQ(MemError() == noErr);
  1042.         }
  1043.     } else {
  1044.         result = CallMenuSelectProc(gMenuSelectOldUPP, startPt);
  1045.     }
  1046.  
  1047.     MoreAssertQ(gMenuSelectState == kMenuSelectStateNil);
  1048.  
  1049.     (void) SetA4(oldA4);
  1050.     
  1051.     return result;
  1052. }
  1053.  
  1054. static void InsertMySystemMenus(SInt16 whereToInsert)
  1055.     // This routine is called by InsertMenuPatch to actually
  1056.     // insert our list of system menus (stored in gRootSysMenus)
  1057.     // into the system menu list.  It does this by iterating
  1058.     // through the list and, for each element, assigning the menu
  1059.     // a unique ID in the system menu range and then calling through
  1060.     // to the old InsertMenu trap to actually put it in the system menu
  1061.     // list.
  1062. {
  1063.     ItemCount systemMenuCount;
  1064.     ItemCount systemMenuIndex;
  1065.     SInt16 newSystemMenuID;
  1066.     MenuRef thisMenu;
  1067.     
  1068.     // For each menu in our list of system menus...
  1069.     
  1070.     systemMenuCount = CountRootSysMenus();
  1071.     for (systemMenuIndex = 0; systemMenuIndex < systemMenuCount; systemMenuIndex++) {
  1072.     
  1073.         // ... find a unique menu ID in the system menu range...
  1074.         
  1075.         newSystemMenuID = FindUniqueSystemMenuID();
  1076.  
  1077.         // ... fix up the menu ID in the MenuRef we're about to insert...
  1078.  
  1079.         thisMenu = (*gRootSysMenus)[systemMenuIndex].theMenu;
  1080.         MoreAssertQ(ValidateSystemMenuRef(thisMenu));
  1081.         (**thisMenu).menuID = newSystemMenuID;
  1082.         
  1083.         // ... and then insert the menu into the menu list.
  1084.         
  1085.         CallInsertMenuProc(gInsertMenuOldUPP, thisMenu, whereToInsert);
  1086.     }
  1087. }
  1088.  
  1089. static pascal void InsertMenuPatch(MenuHandle theMenu, SInt16 beforeID)
  1090.     // This is our patch on InsertMenu.  The only goal of this
  1091.     // patch is to watch for the first person to insert a system
  1092.     // menu and then, when they do that, turn around and insert
  1093.     // our system menus along with it.  This patch typically
  1094.     // executes at Process Manager startup time as the Help menu
  1095.     // is inserted.  The nice thing about this is that we don't
  1096.     // have to monkey with the system state so that our menus
  1097.     // go into the system menu list; the system has already done
  1098.     // that, our menus just tag along for the ride.
  1099. {
  1100.     long oldA4;
  1101.     
  1102.     oldA4 = SetUpA4();
  1103.     
  1104.     // Our patch should only be installed if InitMoreSystemMenus was successful.
  1105.     MoreAssertQ(gRootSysMenus != nil);
  1106.     
  1107.     CallInsertMenuProc(gInsertMenuOldUPP, theMenu, beforeID);
  1108.     
  1109.     if ( !gHaveInsertedRootSysMenus && IsSystemMenuID( (**theMenu).menuID ) ) {
  1110.  
  1111.         // Remember the system's MenuCInfo in gRootSysMenuCInfo
  1112.         // so that we can set it back later.  We currently
  1113.         // don't use this information but I have a feeling
  1114.         // we'll need it in the future.
  1115.         
  1116.         gRootSysMenuCInfo = LMGetMenuCInfo();
  1117.         
  1118.         // Insert our menus into the system menu list.
  1119.         
  1120.         InsertMySystemMenus((**theMenu).menuID);
  1121.         
  1122.         // We only try to do this once.  If we fail, too bad,
  1123.         // there's no one to report an error to.
  1124.         
  1125.         gHaveInsertedRootSysMenus = true;
  1126.     }
  1127.  
  1128.     (void) SetA4(oldA4);
  1129. }
  1130.  
  1131. /////////////////////////////////////////////////////////////////
  1132. #pragma mark ----- Startup -----
  1133.  
  1134. extern pascal OSStatus InitMoreSystemMenus(void)
  1135.     // See comment in interface part.
  1136. {
  1137.     OSStatus err;
  1138.     UniversalProcPtr gMenuSelectPatchUPP;        // points to MenuSelectPatch
  1139.     UniversalProcPtr gInsertMenuPatchUPP;        // points to InsertMenuPatch
  1140.     
  1141.     // We need to use the callback routine SetUpA4 to set up
  1142.     // A4 in our trap patches.  In preparation for that, we
  1143.     // need to remember A4 in some PC-relative place.  The client
  1144.     // may have already done this, but doing it twice doesn't hurt.
  1145.     //
  1146.     // Note that this translates to a NOP for the CFM build.
  1147.     
  1148.     RememberA4();
  1149.     
  1150.     MoreAssertQ(IsSystemStartup());
  1151.     
  1152.     // To avoid allocating extra pointer blocks for each of our
  1153.     // routine descriptors, we just declare them statically
  1154.     // here, and then setup the gFooUPP pointers to point to
  1155.     // them.
  1156.     //
  1157.     // Of course, for the classic 68K case, we can just use
  1158.     // assign them directly.
  1159.     
  1160.     // Build our routine descriptors.
  1161.     
  1162.     #if TARGET_RT_MAC_CFM
  1163.         {
  1164.             static RoutineDescriptor gDetachIconActionRD = BUILD_ROUTINE_DESCRIPTOR(uppIconActionProcInfo, DetachIconAction);
  1165.             static RoutineDescriptor gMenuSelectPatchRD = BUILD_ROUTINE_DESCRIPTOR(uppMenuSelectProcInfo, MenuSelectPatch);
  1166.             static RoutineDescriptor gInsertMenuPatchRD = BUILD_ROUTINE_DESCRIPTOR(uppInsertMenuProcInfo, InsertMenuPatch);
  1167.             gDetachIconActionUPP = &gDetachIconActionRD;
  1168.             gMenuSelectPatchUPP = &gMenuSelectPatchRD;
  1169.             gInsertMenuPatchUPP = &gInsertMenuPatchRD;
  1170.         }
  1171.     #else
  1172.         gDetachIconActionUPP = (IconActionUPP)    DetachIconAction;
  1173.         gMenuSelectPatchUPP  = (UniversalProcPtr) MenuSelectPatch;
  1174.         gInsertMenuPatchUPP  = (UniversalProcPtr) InsertMenuPatch;
  1175.     #endif
  1176.     
  1177.     // Setup our global state.
  1178.     
  1179.     gMenuSelectState = kMenuSelectStateNil;
  1180.     gRootSysMenus = (RootSysMenuHandle) NewHandleSys(0);
  1181.     err = MemError();
  1182.  
  1183.     // If all goes well, install our patches.
  1184.     
  1185.     if (err == noErr) {
  1186.         gMenuSelectOldUPP = GetToolboxTrapAddress(_MenuSelect);
  1187.         gInsertMenuOldUPP = GetToolboxTrapAddress(_InsertMenu);
  1188.  
  1189.         SetToolboxTrapAddress(gMenuSelectPatchUPP, _MenuSelect);
  1190.         SetToolboxTrapAddress(gInsertMenuPatchUPP, _InsertMenu);
  1191.     }
  1192.     
  1193.     return err;
  1194. }
  1195.  
  1196. /////////////////////////////////////////////////////////////////
  1197.